import type { Object3DExtends } from '@/types';
import * as THREE from 'three';
import type { GLTF } from 'three/examples/jsm/loaders/GLTFLoader';
import type { Material } from 'three';
import { clone } from 'three/examples/jsm/utils/SkeletonUtils';
import type Viewer from '../Viewer';
import type { Animate } from '../Viewer';

export default class BaseModel {
  protected viewer: Viewer;
  public gltf: GLTF;
  public object: THREE.Group;
  /**模型原始材质 */
  public originMaterials: Material[] = [];
  public isSaveMaterial: boolean = false;
  public animaIndex: number = -1;
  public mixer!: THREE.AnimationMixer;
  public clock: THREE.Clock;
  public animaObject!: Animate;

  constructor(gltf: GLTF, viewer: Viewer) {
    this.gltf = gltf;
    this.viewer = viewer;
    this.object = gltf.scene || gltf;

    this.clock = new THREE.Clock();
  }

  /**
  * 设置模型比例
  * @param x 可以只填写一个参数
  * @param y 纵轴缩放
  * @param z 横轴缩放
  */
  public setScalc(x: number, y?: number, z?: number) {
    this.object.scale.set(x, y || x, z || x);
  }

  // 用于获取模型的尺寸大小
  public getLength() {
    // 首先创建了一个 THREE.Box3 类型的变量 box
    const box = new THREE.Box3();
    // box.setFromObject(this.object) 方法将模型对象的边界框信息设置到 box 变量中
    box.setFromObject(this.object);
    // 通过调用 box.getSize(new THREE.Vector3()) 方法获取模型边界框的尺寸，返回一个 THREE.Vector3 对象，该对象包含了模型在 x、y、z 轴上的尺寸大小。
    const size = box.getSize(new THREE.Vector3());
    return size;
  }

  /**
  * 设置模型动画
  * @param i 选择模型动画进行播放
  */
  // 通过这些代码，实现了启动模型动画并在每帧更新动画状态的功能，为模型展示和交互提供了丰富的动画效果。
  public startAnima(i = 0) {
    // 首先将传入的参数 i 赋值给 this.animaIndex 属性，表示选择播放的动画序号。
    this.animaIndex = i;
    // 然后检查是否已经存在动画混合器（this.mixer），如果不存在则创建一个新的 THREE.AnimationMixer 对象并绑定到模型对象上。
    if (!this.mixer) this.mixer = new THREE.AnimationMixer(this.object);
    // 接着判断模型是否包含动画，如果不包含则直接返回。
    if (this.gltf.animations.length < 1) return;
    // 如果模型包含动画，则通过 this.mixer.clipAction(this.gltf.animations[i]).play() 方法播放指定序号的动画。
    this.mixer.clipAction(this.gltf.animations[i]).play();
    // 传入参数需要将函数与函数参数分开，在运行时填入
    // 创建一个 animaObject 对象，其中包含了一个函数 fun 和内容 content，用于在播放动画时传入动画更新函数和相关内容
    this.animaObject = {
      fun: this.updateAnima,
      content: this,
    };
    // 最后调用 viewer.addAnimate(this.animaObject) 方法，将动画对象添加到查看器中进行播放。
    this.viewer.addAnimate(this.animaObject);
  }

  // 还定义了一个私有的 updateAnima 方法，用于更新动画。该方法接收一个参数 e，并调用 e.mixer.update(e.clock.getDelta()) 来更新动画状态，其中 e.mixer 表示动画混合器，e.clock 表示时间控制器。
  private updateAnima(e: any) {
    e.mixer.update(e.clock.getDelta());
  }

  /**
   * 克隆模型
   * @param x
   * @param y
   * @param z
   * @returns {*}
   */
  // 方法接受一个参数 [x, y, z] = [0, 0, 0]，表示默认的克隆模型的位置坐标为原点。
  public cloneModel([x, y, z] = [0, 0, 0]) {
    // 首先通过 { ...this.gltf } 的方式浅拷贝原始模型数据，得到一个新的模型场景对象 newScene
    const newScene = { ...this.gltf };
    // 调用 clone(this.object) 方法克隆模型对象，得到一个新的模型实例 newModel。
    const newModel = clone(this.object);
    // 将新模型的位置设置为传入的坐标 [x, y, z]。
    newModel.position.set(x, y, z);
    // 将新模型添加到查看器的场景中，即 this.viewer.scene.add(newModel)
    this.viewer.scene.add(newModel);
    // 将新模型对象赋值给 newScene.scene 属性，以便后续操作
    newScene.scene = newModel as any;
    // 最后返回一个新的 BaseModel 实例，该实例包含了克隆后的模型场景和查看器对象。
    return new BaseModel(newScene, this.viewer);
  }

  /**
   * 开启模型阴影 数组中移除阴影
   */
  // 方法接受一个参数 names = []，表示需要排除不开启投影阴影的模型名称列表，默认为空数组。
  // 通过这个方法，可以方便地控制哪些模型需要开启投影阴影，为场景渲染和视觉效果提供了更多的定制化选项。
  public openCastShadow(names = []) {
    this.gltf.scene.traverse((model: Object3DExtends) => {
      // 在回调函数中，首先判断当前对象是否为网格对象（model.isMesh）并且不在排除的模型名称列表中（!names.includes(model.name as never)）
      if (model.isMesh && !names.includes(model.name as never)) {
        // 如果满足条件，则将对象的 frustumCulled 属性设置为 false，表示不进行视锥体裁剪
        model.frustumCulled = false;
        // 同时将对象的 castShadow 属性设置为 true，表示开启投影阴影。
        model.castShadow = true;
      }
    });
  }

  /**设置模型颜色 */
  public setColor(color = 'yellow', opacity = 0.5) {
    // 首先判断是否需要保存原始材质，如果 this.isSaveMaterial 为假，则将 this.originMaterials 置为空数组。
    if (!this.isSaveMaterial) this.originMaterials = [];
    this.gltf.scene.traverse((model: Object3DExtends) => {
      if (model.isMesh) {
        // 如果不需要保存原始材质，则将当前对象的材质添加到 this.originMaterials 数组中。
        if (!this.isSaveMaterial) this.originMaterials.push(model.material as Material);
        // 为当前对象创建一个新的 THREE.MeshPhongMaterial 材质，并设置其属性：
        model.material = new THREE.MeshPhongMaterial({
          // side 表示双面渲染。
          side: THREE.DoubleSide,
          // transparent 表示开启透明。
          transparent: true,
          // depthTest 表示深度测试
          depthTest: false,
          // depthWrite 表示深度写入，设置为 true 表示不会被鼠标穿透。
          depthWrite: true, // 无法被选择，鼠标穿透
          // color 表示颜色，使用传入的颜色值。
          color: new THREE.Color(color),
          // opacity 表示透明度，使用传入的透明度值。
          opacity: opacity,
        });
      }
    });
    // 最后将 this.isSaveMaterial 置为真，表示已保存原始材质。
    this.isSaveMaterial = true;
  }

  /**设置模型材质 */
  public setMaterial(material = new THREE.MeshBasicMaterial()) {
    if (!this.isSaveMaterial) this.originMaterials = [];
    this.gltf.scene.traverse((model: Object3DExtends) => {
      if (model.isMesh) {
        // 如果不需要保存原始材质，则将当前对象的材质添加到 this.originMaterials 数组中。
        if (!this.isSaveMaterial) this.originMaterials.push(model.material as Material);
        // 将当前对象的材质替换为传入的 material 参数。
        model.material = material;
      }
    });
    // 最后将 this.isSaveMaterial 置为真，表示已保存原始材质。
    this.isSaveMaterial = true;
  }

  // 还原模型材质
  public setDefault() {
    let i = 0;
    this.gltf.scene.traverse((model: Object3DExtends) => {
      if (model.isMesh) {
        model.material = this.originMaterials[i];
        i++;
      }
    });
  }
}
